/**
* Copyright (C) 2008 Abiquo Holdings S.L.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.abiquo.apiclient.stream;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Throwables.getStackTraceAsString;
import static org.atmosphere.wasync.Event.CLOSE;
import static org.atmosphere.wasync.Event.MESSAGE;
import java.io.Closeable;
import java.io.IOException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Logger;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import org.atmosphere.wasync.ClientFactory;
import org.atmosphere.wasync.Function;
import org.atmosphere.wasync.Request.METHOD;
import org.atmosphere.wasync.Request.TRANSPORT;
import org.atmosphere.wasync.Socket;
import org.atmosphere.wasync.impl.AtmosphereClient;
import org.atmosphere.wasync.impl.AtmosphereRequest;
import com.abiquo.event.json.module.AbiquoModule;
import com.abiquo.event.model.Event;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.introspect.AnnotationIntrospectorPair;
import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector;
import com.google.common.base.Throwables;
import com.ning.http.client.AsyncHttpClient;
import com.ning.http.client.AsyncHttpClientConfig;
import com.ning.http.client.Realm;
public class StreamClient implements Closeable
{
private static final Logger LOG = Logger.getLogger("abiquo.stream");
private final String endpoint;
private final String username;
private final String password;
private final SSLConfiguration sslConfiguration;
private final Consumer<Event> consumer;
private boolean reconnect = false;
private int reconnectAttempts = 0;
private int pauseBeforeReconnectInSeconds = 0;
private final Runnable beforeReconnection;
private final Runnable afterReconnection;
// no configs
private final ObjectMapper json;
private AsyncHttpClient asyncClient;
private Socket socket;
private AtomicBoolean manuallyClosing = new AtomicBoolean(false);
private Executor reconnectionExecutor = Executors.newCachedThreadPool();
// Do not use directly. Use the builder.
private StreamClient(final String endpoint, final String username, final String password,
final SSLConfiguration sslConfiguration, final Consumer<Event> consumer,
final Runnable beforeReconnection, final Runnable afterReconnection,
final boolean reconnect, final int reconnectAttempts,
final int pauseBeforeReconnectInSeconds)
{
this.endpoint = checkNotNull(endpoint, "endpoint cannot be null");
this.username = checkNotNull(username, "username cannot be null");
this.password = checkNotNull(password, "password cannot be null");
this.consumer = checkNotNull(consumer, "consumer cannot be null");
this.sslConfiguration = sslConfiguration;
this.reconnect = checkNotNull(reconnect);
if (this.reconnect)
{
this.reconnectAttempts = checkNotNull(reconnectAttempts,
"reconnect attempts cannot be null if reconnection has been enabled");
this.pauseBeforeReconnectInSeconds = checkNotNull(pauseBeforeReconnectInSeconds,
"pause seconds before reconnect cannot be null if reconnection has been enabled");
this.beforeReconnection = beforeReconnection;
this.afterReconnection = afterReconnection;
}
else
{
this.beforeReconnection = null;
this.afterReconnection = null;
}
json = new ObjectMapper()
.setAnnotationIntrospector( //
new AnnotationIntrospectorPair(new JacksonAnnotationIntrospector(),
new JaxbAnnotationIntrospector(TypeFactory.defaultInstance()))) //
.registerModule(new AbiquoModule());
}
public void connect() throws IOException
{
checkState(socket == null, "the client is already listening to events");
checkState(asyncClient == null, "the client is already listening to events");
LOG.fine("Connecting to " + endpoint + "...");
AtmosphereClient client = ClientFactory.getDefault().newClient(AtmosphereClient.class);
AtmosphereRequest request = client.newRequestBuilder() //
.method(METHOD.GET) //
.uri(endpoint + "?Content-Type=application/json") //
.transport(TRANSPORT.SSE) //
.transport(TRANSPORT.LONG_POLLING).build();
AsyncHttpClientConfig.Builder config = new AsyncHttpClientConfig.Builder();
config.setRequestTimeoutInMs(-1);
config.setIdleConnectionTimeoutInMs(-1);
if (sslConfiguration != null)
{
config.setHostnameVerifier(sslConfiguration.hostnameVerifier());
config.setSSLContext(sslConfiguration.sslContext());
}
config.setRealm(new Realm.RealmBuilder() //
.setPrincipal(username) //
.setPassword(password) //
.setUsePreemptiveAuth(true) //
.setScheme(Realm.AuthScheme.BASIC) //
.build());
asyncClient = new AsyncHttpClient(config.build());
socket = client.create(client.newOptionsBuilder().runtime(asyncClient).build());
socket.open(request);
configure();
LOG.fine("Connected!");
}
private void configure()
{
socket.on(MESSAGE, new Function<String>()
{
@Override
public void on(final String rawEvent)
{
try
{
Event event = json.readValue(rawEvent, Event.class);
consumer.accept(event);
}
catch (IOException ex)
{
LOG.warning(String.format("Unexpected error processing event: %s\n%s",
ex.getMessage(), getStackTraceAsString(ex)));
}
}
})
.on(CLOSE, new Function<String>()
{
@Override
public void on(final String rawEvent)
{
if (!manuallyClosing.get() && reconnect)
{
reconnectionExecutor.execute(new Runnable()
{
@Override
public void run()
{
try
{
if (beforeReconnection != null)
{
beforeReconnection.run();
}
LOG.warning("Connection lost, going to reconnect");
reconnect();
LOG.fine("Reconnected");
if (afterReconnection != null)
{
afterReconnection.run();
}
}
catch (IOException e)
{
throw Throwables.propagate(e);
}
}
});
}
}
});
}
private void reconnect() throws IOException
{
int retry = 0;
while (retry < reconnectAttempts)
{
try
{
closeConnection();
connect();
return;
}
catch (IOException e)
{
retry++;
try
{
Thread.sleep(pauseBeforeReconnectInSeconds * 1000);
}
catch (InterruptedException e1)
{
throw Throwables.propagate(e1);
}
}
}
LOG.severe("Reconnection failed");
closeConnection();
}
@Override
public synchronized void close() throws IOException
{
try
{
manuallyClosing.set(true);
closeConnection();
}
finally
{
manuallyClosing.set(false);
}
}
private synchronized void closeConnection() throws IOException
{
LOG.fine("Disconnecting...");
if (asyncClient != null)
{
asyncClient.close();
}
if (socket != null)
{
socket.close();
}
asyncClient = null;
socket = null;
LOG.fine("Disconnected!");
}
public static Builder builder()
{
return new Builder();
}
public static class Builder
{
private String endpoint;
private String username;
private String password;
private SSLConfiguration sslConfiguration;
private Consumer<Event> consumer;
private boolean reconnect = false;
private int reconnectAttempts = 10;
private int pauseBeforeReconnectInSeconds = 5;
private Runnable beforeReconnect;
private Runnable afterReconnect;
public Builder endpoint(final String endpoint)
{
this.endpoint = endpoint;
return this;
}
public Builder credentials(final String username, final String password)
{
this.username = username;
this.password = password;
return this;
}
public Builder sslConfiguration(final SSLConfiguration sslConfiguration)
{
this.sslConfiguration = sslConfiguration;
return this;
}
public Builder consumer(final Consumer<Event> consumer)
{
this.consumer = consumer;
return this;
}
public Builder reconnect(final boolean reconnect)
{
this.reconnect = reconnect;
return this;
}
public Builder reconnectAttempts(final int attempts)
{
this.reconnectAttempts = attempts;
return this;
}
public Builder pauseBeforeReconnectInSeconds(final int seconds)
{
this.pauseBeforeReconnectInSeconds = seconds;
return this;
}
public Builder beforeReconnect(final Runnable beforeReconnect)
{
this.beforeReconnect = beforeReconnect;
return this;
}
public Builder afterReconnect(final Runnable afterReconnect)
{
this.afterReconnect = afterReconnect;
return this;
}
public StreamClient build()
{
return new StreamClient(endpoint,
username,
password,
sslConfiguration,
consumer,
beforeReconnect,
afterReconnect,
reconnect,
reconnectAttempts,
pauseBeforeReconnectInSeconds);
}
}
public static interface SSLConfiguration
{
/**
* Provides the SSLContext to be used in the SSL sessions.
*/
public SSLContext sslContext();
/**
* Provides the hostname verifier to be used in the SSL sessions.
*/
public HostnameVerifier hostnameVerifier();
}
}